Реализовать `hash_set` - упрощённый вариант `std::unordered_set` с поддержкой:

* deduction guides
* slicing
* гетерогенный поиск
* strong exceptions guarantee (каждая операция либо завершилась успешно либо не изменила `hash_set`, выбросив исключение)

<br />

Требования:

* шаблонные параметры: `Item` - тип хранимых значений, `HashFunction` - расчёт хеша, `ItemEqual` - сравнивалка элементов

```c++
template<typename Item,
         typename HashFunction = std::hash<Item>,
         typename ItemEqual = std::equal_to<Item>>
class hash_set { ... };
```

* внутри `hash_set` - массив корзинок.
  * в корзинке элементы могут храниться как в plain-варианте, так и по указателю.

* все обычные конструкторы, присваивания, сравнения

* `hash_set` нужно уметь создавать:
  * пустым (по умолчанию)
  * от `std::initializer_list<Item>`
  * от `std::initializer_list<T>`, если `T` конвертируется в `Item`
  * от пары итераторов `[begin, end)`. Итераторы могут быть любого типа, хоть от `std::vector<T>`, хоть от `std::set<T>`. Требуется, чтобы `T` был приводим к `Item`
  * если `T` неприводим к `Item`, выдайте красивое сообщение об ошибке через `static_assert` (см. `std::is_convertible`)

* реализовать методы:
  * `empty` - проверка на пустоту
  * `size` - кол-во элементов
  * `reserve(N)` - аллоцировать необходимое кол-во корзинок, чтобы контейнер вмещал `N` элементов без рехешинга
  * `std::pair<iterator, bool> add` - добавить элемент в множество. Возвращает итератор на элемент и метку, был ли он в множестве до вызова `add`.
  * `std::pair<iterator, bool> emplace` - добавить элемент в множество, конструируя его из аргументов `emplace`. Возвращает итератор на элемент и метку, был ли он в множестве до вызова `add`. Здесь нужно реализовать perfect forwarding.
  * `bool del` - удалить элемент, возвращает `true`, если элемент был действительно удалён

* forward-итерирование по `hash_set` (порядок не гарантирован)

* `contains` - проверка, что множество содержит элемент. `contains` должен быть гетерогенным, т.е.:

```c++
hash_set<std::string, MAYBE_HETEROGENEOUS_MARKER_SOMEHOW_I_DO_NOT_KNOW> myset = { "abc", "def", "ghi" };
std::cout << myset.contains("def"); // не создаётся промежуточной строки
```

* реализовать slicing - перебрасывание нод из одного множества в другое:
  * `extract` - извлечение ноды по итератору или гетерогенно
  * `insert` - добавление ноды
  * `merge(another_set)` - извлечение всех нод из `another_set`, которых нет в `this` (не забудьте про strong exception guarantee, здесь, скорее всего, ваш код будет очень далёк от эффективного варианта с более слабыми гарантиями исключений)

```c++
// организуйте механизм таким образом, чтобы в этом коде не было утечек:
{
  auto node_handle = myset.extract("abc");
  // |node_handle| никуда не вставляется
}
```

* deduction guides должны автоматически выводить тип `hash_set` из `std::initializer_list` и пары итераторов `[begin, end)`.

<br />

Замечания:

* при проверке strong exception guarantee предполагайте, что `~Item` и `HashFunction` не бросают исключений.
* как реализовывать гетерогенный поиск - можете подглядеть в libstdc++ / libc++, но, скорее всего, ваша реализация будет проще
* при разработке гетерогенности предполагайте, что если `Item` чему-то равен через `ItemEqual`, то у них одинаковый результат хеша
* не забудьте про насыщение и `rehash`.
* **не забудьте про unit-тесты**

